package mkv.MyGUI;
import processing.core.*; 

import java.awt.event.KeyEvent;

/**
 * A non visible grouping object that contains other MyGUI elements.
 * MyGUIGroup allows objects to be grouped so that they can be moved, rotated, enabled,
 * disabled and hidden as a group.
 * MyGUIGroup objects difer from traditional Panel objects because they have no width
 * or height, and so do not crop objects that are out of bounds.
 * <p>
 * Objects placed in the group will be translated, rotated, and scaled before their
 * own draw methods are called. MyGUIGroup also holds the responsibility of passing Mouse
 * and Key events to members of the group. Part of this involves filtering
 * events based on states such as _disabled and _visible. In other words, it only passes
 * Key and Mouse events to objects which are visible and not disabled.
 * <p>
 * MyGUIGroup plays a larger role in the sense that the MyGUI object inherits many of its
 * methods from the MyGUIGroup class. Originally it was the case that the code from MyGUI
 * was used to build MyGUIGroup but this was obviously leading to a lot of redundancy.
 * 
 * @author John Beech
 * @see MyGUIObject
 * @see MyGUI
 * 
 */
public class MyGUIGroup extends MyGUIObject {
  /**
   * Object array storign references to potential children
   */
  private MyGUIObject[] elements;
  /**
   * The array index of the child that currently has focus
   */
  private int focusIndex = -1;
  /**
   * The number of active elements or used and removed elements in the array
   */
  private int numElements = 0;
  /**
   * State if user wants to use custom mouse coordinates
   */
  protected boolean customMouse = false;
  
  /**
   * Default class constructor. 
   * @param root  a direct reference to the main processing PApplet.
   */
  public MyGUIGroup(PApplet root) {
      super(root, 0, 0);
      elements = new MyGUIObject[25];
      this._style = new MyGUIStyle(_root);
  }
  
  /**
   * Positional class constructor. 
   * @param root  a direct reference to the main processing PApplet.
   * @param x     x coordinate of the group
   * @param y     y coordinate of the group
   */
  public MyGUIGroup(PApplet root, int x, int y) {
    super(root, x, y);
    elements = new MyGUIObject[25];
    this._style = new MyGUIStyle(_root);    
  }
  /**
   * Class constructor where the size of the element array can be specified.
   * @param root  a direct reference to the main processing PApplet.
   * @param x     x coordinate of the group
   * @param y     y coordinate of the group
   * @param size  size of the element array. The default size is 25.
   */
  public MyGUIGroup(PApplet root, int x, int y, int size) {
    super(root, x, y);
    elements = new MyGUIObject[size];
    this._style = new MyGUIStyle(_root);
  }
  /**
   * Class constructor where you can specify the size of the element array and set
   * the style.
   * @param root  a direct reference to the main processing PApplet.
   * @param x     x coordinate of the group
   * @param y     y coordinate of the group
   * @param size  size of the element array. The default size is 25.
   * @param style personalized style for this object and its children
   */
  public MyGUIGroup(PApplet root, int x, int y, int size, MyGUIStyle style) {
    super(root, x, y);
    elements = new MyGUIObject[size];
    this._style = style;
  }
  
  /**
   * Class constructor where you can specify the style but the element array
   * stays at its default value.
   * @param root  a direct reference to the main processing PApplet.
   * @param x     x coordinate of the group
   * @param y     y coordinate of the group
   * @param style personalized style for this object and its children
   */
  public MyGUIGroup(PApplet root, int x, int y, MyGUIStyle style) {
    super(root, x, y);
    elements = new MyGUIObject[25];
    this._style = style;
  }
   
  /* Methods which affect the properties of the collection */
  /**
   * Adds a compatable MyGUI object as a child.
   * I'll explain how this work in conjunction with the remove method.
   * <p>
   * When adding a new element, add first checks to see if there are any null entries
   * in the element array caused by sudden removals. If one is found this will be used
   * first. If there were no null elements and the number of elements is less then the 
   * size of the array, then the new element will be added first available place before
   * the end of the array. If there are no spaces left in the array, this method will
   * return -1.
   * <p>
   * When an object has been successfully added, the setParent() and setID() methods are
   * called in the new object passing information down the hierarchy from parent to child.
   * 
   * @return The id of the new object unique to this group. Returns -1 if the object
   *         could not be added.
   *         
   * @param newElement A reference to the MyGUIObject to be added to the group.
   * 
   * @see #remove(int)
   * @see MyGUIObject#setParent(MyGUIObject)
   * @see MyGUIObject#setID(int)
   */
  public int add(MyGUIObject newElement) {
    // See if there are any null entries to replace
    for(int i=0; i<numElements; i++) {
      if(elements[i] == null) {
        elements[i] = newElement;
        elements[i].setParent(this);
        elements[i].setID(i);
        return i;
      }
    }
    // If there were no null elements, then add to the end (providing there is space)
    if(numElements < elements.length) {
      // Assign new element to array and increment element counter
      int elementID = numElements;
      numElements++;
      elements[elementID] = newElement;
      elements[elementID].setParent(this);
      elements[elementID].setID(elementID);
      return elementID;
    }
    return -1;
  }  

  /**
   * Removes an element by its id. This id matches the id returned by the add method. When
   * an element is removed its slot is set to null. The object is not destroyed from memory
   * though, and you may have a reference to it elsewhere in the code. If you simlpy want
   * to hide an element and stop it processing try setting the _visible state to false.
   * 
   * @param id The id of the element to be removed.
   * 
   * @see MyGUIObject#_visible
   * @see #add(MyGUIObject)
   */
  public void remove(int id) {
    if(id > 0 && id <= numElements) {
      elements[id] = null;
    }
  }

  /**
   * Sets the style of an object after initilisation. Used to set a custom style
   * for an object, and all its children. This works by reference, where the children
   * look to their parent if they do not have their own style.
   * 
   * @param style A reference to the new personalized style.
   */
  public void setStyle(MyGUIStyle style) {
    this._style = style;
  }
      
  /**
   * Tells MyGUI that you want to use custom mouse coordinates. Use in conjunction with
   * setMouseCoords(), this tells updateLocalMouse() not to update tmouseX and tmouseY with
   * values from the parent object. 
   * 
   * @param mode Set true to use custom mouse. Set false to use parental mouse coordinates.
   * @see #setMouseCoords(int, int)
   * @see #updateLocalMouse()
   */
  public void useCustomMouse(boolean mode) {
    customMouse = mode;
  }
  
  /**
   * Allows the user to specify custom mouse coordinates. The method useCustomMouse(true) must be
   * called before this has any affect. It is the user's responsibility to constantly
   * update these mouse coordinates.
   * 
   * @param x the x coordinate of the mouse relative to this shape
   * @param y the y coordinate of the mouse relative to this shape
   */
  public void setMouseCoords(int x, int y) {
    tmouseX = x;
    tmouseY = y;
    // Apply relative rotation/scale to these values
    updateLocalMouse();
    
  }

  /* Methods which return values about the collection */
  /**
   * Returns a reference to the MyGUIObject that currently has focus.
   * It will return null if no element is currently tracked.
   * 
   * @return returns a reference to the MyGUIObject that currently has focus. 
   * 
   * @see #get(int)
   * @see #getFocusIndex()
   */
  public MyGUIObject getFocused() {
    if(focusIndex != -1) {
      return elements[focusIndex];
    }
    return null;
  }
  
  /**
   * Returns the id/index of the currently focused element. If no element
   * has focus then -1 will be returned. 0 is a valid index/id for an element.
   * You may need to check that id returned doesn't reference a group object that contains
   * other elments, because there may be another focused child inside it.
   * 
   * @return the id or index of the current focused element.
   * 
   * @see #getFocused()
   * @see #get(int)
   */
  public int getFocusIndex() {
    return focusIndex;
  }

  /**
   * Allows the focus index to be updated manually, or by a child item that has gained
   * focus. Returns true when the index has been set.
   * 
   * @return  true when the index has been set
   * 
   * @param index  the new index to be set.
   * 
   * @see #MyGUIObject.setFocusIndex(int index)
   */
  public boolean setFocusIndex(int index) {
      focusIndex = index;
      return true;
  }
  
  /**
   * Return an object based on the supplied index or id. 
   * @param index the index or id of the object when it was added to the element array.
   * 
   * @return returns a reference
   * 
   * @see #getFocused()
   * @see #getFocusIndex()
   */
  public MyGUIObject get(int index) {
    if(index < numElements && index > 0) {
      return elements[focusIndex];
    }
    return null;
  }

  /**
   * Returns true if there are no elements in the group.
   * @return returns true if there are no elements in the group.
   * 
   * @see #isFull()
   */
  public boolean isEmpty() {
      if(numElements == 0) {
          return true;
      } else {
          for(int i=0; i<numElements; i++) {
              if(elements[i] == null) {
                  return true;
              }
          }
          return false;
      }
  }

  /**
   * Returns true if the element array is full. First checks the numElements value for
   * spare length at the end of the array, and then checks for null slots created by 
   * the remove() method.
   * 
   * @return returns true if the element array is full
   * 
   * @see #isEmpty()
   * @see #remove(int)
   */
  public boolean isFull() {
    // quick check, space at end of array?
    if(numElements != elements.length) {
      return false;
    }
    // longer check for null spaces
    for(int i=0; i<numElements; i++) {
      if(elements[i] == null) return false;
    }
    // If numElements == elements.length and there are no null entries:
    return true;
  }  
      
  /**
   * Notifies children of the mousePressed event.
   * When this method is called a single element may MyGUIGroup be given focus.
   * Focus is a state necessary for a GUI element to perform actions. Elements with higher
   * indexes will get focus priority over lower indexes. Higher index elements
   * are also drawn last and so will appear on-top of other objects.
   * The focus needs to be decided prior to passing mousePressed as mousePressed may rely
   * on the state of focusIndex before it can work properly.
   * <p>
   * The method first filters based on the _visible and _disabled states. If
   * either of these are true then no action will be taken, stopping the child elements
   * from receiving this event.
   * <p>
   * The focus index is cleared, set to -1. It then loops backwards through the element
   * array calling checkForHit() on each element and testing _disabled and _visible for
   * each object. If checkForHit() is true, and the element is not disabled and is visible
   * then the element will be given focus and the loop stopped. Only one element may have
   * focus. 
   * <p>
   * The array is looped through again this time to pass the mousePressed() call onto all
   * the visible and active children. This is required so objects know when the mouse has
   * been released outside their boundary.
   * 
   * @see MyGUIObject#mousePressed()
   * @see MyGUIObject#checkForHit()
   * @see MyGUIObject#_visible
   * @see MyGUIObject#_disabled
   */
  public void mousePressed() {
      if(_visible && !_disabled) {
          // clear focus (will be set again below)
          focusIndex = -1;
          // loop through collection of objects and pass on call to mousePressed
          for(int i=numElements-1; i>=0; i--) {
              // keep track of which element has focus
              if(elements[i] != null) {
                  // If the element is underneath the cursor, and the element is not disabled, and it is visible.
                  if(elements[i].checkForHit() && elements[i]._visible && !elements[i]._disabled) {
                      // match found
                      focusIndex = i;
                      // kill loop
                      i = -1;
                      // tell parent object that this group has focus
                      if(_parent != null) {
                          _parent.setFocusIndex(_id);
                      }
                  }
              }
          }
          // loop again after focusIndex has been decided
          for(int i=0; i<numElements; i++) {
              // pass on mousePressed command to elements in array
              // but only if they exist (!= null)
              // and only if they are not disabled
              if(elements[i] != null)
                  if(!elements[i]._disabled && elements[i]._visible)
                      elements[i].mousePressed();
          }
      }
  }

  /**
   * Notifies children of the mouseReleased event. The method also filters
   * non visible and disabled children, stopping them from receiving this event.
   * 
   * @see MyGUIObject#mouseReleased()
   * @see MyGUIObject#_visible
   * @see MyGUIObject#_disabled
   */
  public void mouseReleased() {
      if(_visible && !_disabled) {
          // Loop through collection of objects and pass on call to mouseReleased
          for(int i=0; i<numElements; i++) {
              // but only if they exist (!= null)
              // and only if they are not disabled
              if(elements[i] != null)
                  if(!elements[i]._disabled && elements[i]._visible )
                      elements[i].mouseReleased();
          }
      }
  }

  /**
   * Notifies children of the mouseDragged event. Like mouseReleased() the method
   * also filters visible and disabled children, stopping them from receiving this event.
   * 
   * @see MyGUIObject#mouseDragged()
   * @see MyGUIObject#_visible
   * @see MyGUIObject#_disabled
   */
  public void mouseDragged() {
      if(_visible && !_disabled) {
          // Loop through collection of objects and pass on call to mouseDragged
          for(int i=0; i<numElements; i++) {
              // but only if they exist (!= null)
              // and only if they are not disabled
              if(elements[i] != null)
                  if(!elements[i]._disabled && elements[i]._visible)
                      elements[i].mouseDragged();
          }
      }
  }

  /* Methods for handling key events */
  /**
   * Notifies the focus child of the keyPressed event. 
   * Unlike mouse events, KeyEvents will only be sent to the object that has focus.
   * 
   * @see MyGUIObject#keyPressed(KeyEvent)
   * @see java.awt.event.KeyEvent
   */
  public void keyPressed(KeyEvent e) {
      if(_visible && !_disabled) {
          if(focusIndex >= 0 && focusIndex < numElements) {
              if(elements[focusIndex] != null) {
                  if(elements[focusIndex]._visible && !elements[focusIndex]._disabled) {
                      elements[focusIndex].keyPressed(e);
                  }
              }
          }
      }
  }
  
  /**
   * Notifies the focus child of the keyReleased event. 
   * Unlike mouse events, KeyEvents will only be sent to the object that has focus.
   * 
   * @see MyGUIObject#keyReleased(KeyEvent)
   * @see java.awt.event.KeyEvent
   */
  public void keyReleased(KeyEvent e) {
      if(_visible && !_disabled) {
          if(focusIndex >= 0 && focusIndex < numElements) {
              if(elements[focusIndex] != null) {
                  if(elements[focusIndex]._visible && !elements[focusIndex]._disabled) {
                      elements[focusIndex].keyReleased(e);
                  }
              }
          }
      }
  }
  
  /**
   * Notifies the focus child of the keyTyped event. 
   * Unlike mouse events, KeyEvents will only be sent to the object that has focus.
   * 
   * @see MyGUIObject#keyTyped(KeyEvent)
   * @see java.awt.event.KeyEvent
   */
  public void keyTyped(KeyEvent e) {
      if(_visible && !_disabled) {      
	      if(focusIndex >= 0 && focusIndex < numElements) {
	          if(elements[focusIndex] != null) {
	              if(elements[focusIndex]._visible && !elements[focusIndex]._disabled) {
	                  elements[focusIndex].keyTyped(e);
	              }
	          }
	       }
      }
  }
  
  /* Draw */
  /**
   * Registered method that tells its children to draw.
   * This draw method performs several tasks. Firstly it changes the drawing
   * matrix by translating, rotating and then scaling. Secondly it updates the local
   * mouse coordinates relative to the translation, rotatation and scaled applied to
   * its parent (<i>see updateLocalMouse()</i>). Thirdly it loops through all its child
   * elements and calls their draw methods. Finally it calls the draw method of the
   * element that has focus (if any) which means that the object that has
   * focus will be drawn on top of  everything else. 
   *
   * @see #updateLocalMouse()
   */
  public void draw() {
    if(_visible) {
	    // The whole coordinate system needs moving, rotating and scaling
	    _root.pushMatrix();
	    _root.translate(_x, _y);
	    _root.rotate(PApplet.radians(_rotation));
	    _root.scale(_scale);
	    updateLocalMouse();
	    // Loop through collection of objects and call their draw methods
	    for(int i=0; i<numElements; i++) {
	      // Check element isn't the focus object (i != focusindex)
	      // and check there is an element at this position (!= null)
	      if(i != focusIndex && elements[i] != null)
	        // Check if object is visible
	        if(elements[i]._visible)
	          elements[i].draw();
	    }
	    if(focusIndex >= 0 && focusIndex < numElements) {
	      if(elements[focusIndex] != null) {
	        if(elements[focusIndex]._visible)
	          elements[focusIndex].draw();
	      }
	    }
	    // reset matrix, end of group draw
	    _root.popMatrix();
	  }
  }
  
  /**
   * Calculates the local relative mouse coordinates. By local this means the tmouseX
   * and tmouseY coordinates are set relative to the _x, _y position of this object.
   * These are then used in public boolean checkForHit(); Note, the values are actually
   * relative to the parent. You may need to multiply these values by scale
   * if the current object has been scaled and you need the onscreen position.
   * <p>
   * This only applies if customMouse state is not set to true. If it is set, tmouseX and
   * tmouseY are not updated and instead values used/set by setMouseCoords(). 
   * 
   * @see #useCustomMouse(boolean)
   * @see #setMouseCoords(int, int)
   * @see MyGUIObject#checkForHit()
   */
  public void updateLocalMouse() {
      // This code was taken from MyGUIObject.updateLocalMouse() and altered
      // to support the customMouse state. It calculates the apparent non rotated
      // and non translated mouse coordinates.
      if(customMouse) {
          // If a custom mouse is provided, it will likely still need rotating
          if(_rotation != 0) {
              float cosval = _root.cos(-PApplet.radians(_rotation));
              float sinval = _root.sin(-PApplet.radians(_rotation));
              int rotationPointX = (tmouseX - _x);
              int rotationPointY = (tmouseY - _y);
              tmouseX = PApplet.round(rotationPointX * cosval - rotationPointY * sinval * _parent._scale);
              tmouseY = PApplet.round(rotationPointX * sinval + rotationPointY * cosval * _parent._scale);
          }
      } else
      if(_rotation != 0) {
          float cosval = _root.cos(-PApplet.radians(_rotation));
          float sinval = _root.sin(-PApplet.radians(_rotation));
          int rotationPointX = (_parent.tmouseX - _x);
          int rotationPointY = (_parent.tmouseY - _y);
          tmouseX = PApplet.round(rotationPointX * cosval - rotationPointY * sinval * _parent._scale);
          tmouseY = PApplet.round(rotationPointX * sinval + rotationPointY * cosval * _parent._scale);
      } 
      else {
          // No rotation, just use the values
          tmouseX = PApplet.round((_parent.tmouseX - _x) * _parent._scale);
          tmouseY = PApplet.round((_parent.tmouseY - _y) * _parent._scale);
      }
  }
}